import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_demo/pages/animation/animation_page.dart';
import 'package:flutter_demo/pages/page_with_title.dart';

import '../../widgets/app_widgets.dart';

class CustomPaintPage extends PageWithTitle {
  const CustomPaintPage() : super("自定义绘制(CustomPaint)");

  @override
  List<Widget> buildContentWidgets(BuildContext context) {
    return const [
      PrimaryTitle("简介"),
      BodyText("通过自定义组件，覆写renderObject.paint在画布上进行绘制，实现自定义组件"),
      LabelText("- 实际上flutter框架提供了一些基础的封装，可以让开发者快速实现自定义绘制"),
      Row(
        children: [
          SizedBox(
            width: 80,
            height: 80,
            child: CircleLoading(),
          ),
        ],
      ),
      AppSpacer.large,
      UlItem("CustomPaint"),
      BodyText(
          "自定义画笔，实现静态绘制，该组件在RenderCustomPaint对绘制过程进行了简易封装，开发者只需通过CustomPainter实现绘制过程与判断是否需要重绘的即可"),
      LabelText("- painter，组件内容绘制者"),
      LabelText(
          "- foregroundPainter，前景内容绘制者，因为组件支持嵌入子组件，在渲染对象中分三个步骤先绘制组件内容，再绘制子组件，最后绘制前景内容"),
      LabelText("- child，支持内嵌子组件"),
      LabelText("- size，默认尺寸（默认是Zero）"),
      AppSpacer.large,
      UlItem("CustomPainter"),
      BodyText("自定义绘制者，实现以下关键方法即可快速完成更新与绘制"),
      OlItem(1, "shouldRepaint"),
      BodyText("是否应该重新绘制，在渲染对象更新是调用，开发者可对比与上个渲染对象的参数差异来决定是否要重新绘制"),
      OlItem(2, "paint"),
      BodyText("绘制方法，方法接收两个参数，分别为画布与组件实际尺寸，开发者将内容绘制到canvas即可，相当简单"),
      AppSpacer.large,
      PrimaryTitle("如何实现动态更新"),
      BodyText(
          "这需要用到flutter动画相关的api，本质是通过状态组件在条件（如时间、用户行为）变化时触发rebuild，构建新的CustomPaint"),
      AppSpacer.large,
      UlItem("AnimatedBuilder"),
      BodyText("动画构建器，通过监听外部条件改变，内部通过setState触发重建来实现"),
      LabelText(
          "- animation，动画触发器，AnimatedBuilder会设置一个监听给他，实现动画刷新，它的常用实现就是AnimationController"),
      LabelText("- child，支持插入子组件，在动画刷新时，可以自己结合child来构建新的组件"),
      LabelText(
          "- builder，构造组件的实现方法，若使用时没有传入child，则这里可以直接无视child，我们只需构建新的CustomPaint来绘制当前帧的内容即可"),
      AppSpacer.large,
      UlItem("AnimationController"),
      BodyText("动画控制器，参考基础动画中关于AnimationController的介绍"),
      PageLink(AnimationPage()),
    ];
  }
}

class CircleLoading extends StatefulWidget {
  ///循环颜色列表
  final List<Color> colors;

  ///循环一次的动画时长(毫秒)
  final int duration;

  const CircleLoading({
    super.key,
    this.colors = const [
      Color.fromRGBO(255, 0, 0, 1),
      Color.fromRGBO(0, 255, 0, 1),
      Color.fromRGBO(0, 0, 255, 1),
    ],
    this.duration = 2000,
  });

  @override
  State<StatefulWidget> createState() {
    return _CircleLoadingState();
  }
}

/// 动画组件的状态
/// - [SingleTickerProviderStateMixin] 单一触发器提供者，用于触发动画刷新
///   - 内部创建Ticker，通过[scheduleTick]方法实现监听屏幕刷新来回调动画刷新
///   - 触发器提供者内部监听了组件是否处于显示状态来控制是否继续监听屏幕刷新
///   - 这样开发者就不用手写大量与生命周期控制相关的代码，若组件中需要多个动画控制器，可以混入
///   [TickerProviderStateMixin]来实现多动画与组件生命周期同步
class _CircleLoadingState extends State<CircleLoading>
    with SingleTickerProviderStateMixin {
  ///动画控制器，实现动画配置与进度计算
  /// - 由于state跟随element的生命周期，我们可以在初始化时创建动画控制器，如果在build中创建会
  /// 因为widget刷新导致重复创建动画控制器
  /// - 销毁时也要记得销毁动画控制器，内部会销毁ticker、注销动画监听
  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    //初始化动画控制器，将其设置为重复执行，其构造函数可以设置数值区间，默认[0,1]
    _animationController = AnimationController(vsync: this);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    //设置动画时长，由于initState还未能确定state与widget绑定，所以在这个方法中实现
    if (_animationController.duration?.inMilliseconds != widget.duration) {
      _animationController.duration = Duration(milliseconds: widget.duration);
      _animationController.repeat();
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    //标记使用单独图层进行局部重绘、据说是支持并发绘制，图层有自己的缓冲画布
    return RepaintBoundary(
      //构建动画，内部逻辑相当简单，收到动画控制器刷新绘制的指令时，执行setState触发build
      //如果是根据其它条件，比如手势之类的，这里也可以嵌套GestureDetector之类的组件，来根据
      // 事件刷新CustomPaint
      child: AnimatedBuilder(
        animation: _animationController,
        builder: (context, child) {
          //屏幕刷新时执行此方法重建组件，这里根据当前动画进度刷新绘制
          return CustomPaint(
            size: const Size(40, 40),
            painter: _LoadingPainter(
              widget.colors,
              _animationController.duration?.inMilliseconds ?? 0,
              _animationController.lastElapsedDuration?.inMilliseconds ?? 0,
              _animationController.value,
            ),
          );
        },
      ),
    );
  }
}

///loading的绘制实现
///- [CustomPainter] 自定义画笔，覆写以下方法实现绘制自定义内容
class _LoadingPainter extends CustomPainter {
  /// 动画循环颜色
  final List<Color> colors;

  ///一次循环的动画时长
  final int duration;

  ///动画总运行时长
  final int lastElapsedDuration;

  /// 动画循环进度[0,1]
  final double progress;

  ///绘制画笔
  final _paint = Paint();

  ///下个颜色与当前颜色
  late int _nextColorIndex;
  late int _currColorIndex;

  _LoadingPainter(
      this.colors, this.duration, this.lastElapsedDuration, this.progress) {
    assert(colors.length > 1);
    assert(duration > 0);
    assert(progress >= 0);

    ///计算从动画开始至今的切换次数，取得下个颜色所对应的颜色位置
    _nextColorIndex =
        (lastElapsedDuration * 4.0 / duration).toInt() % colors.length;
    _currColorIndex = (_nextColorIndex - 1 + colors.length) % colors.length;
  }

  /// 这个方法决定是否需要重新绘制，我们根据动画参数来判断即可
  /// - covariant 协变，在dart中使用斜边可以让子类覆写方法时约束类型到子类，比如这里直接改为
  /// _LoadingPainter在编译时是没有问题的，但运行时如果有别的地方传入了父类的类型可能会出现运
  /// 行时异常。。。算了还是按照正常的面向对象，子类要兼容父类的功能。
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate is! _LoadingPainter ||
        oldDelegate.colors != colors ||
        oldDelegate.duration != duration ||
        oldDelegate.lastElapsedDuration != lastElapsedDuration ||
        oldDelegate.progress != progress;
  }

  ///当前处于循环中的哪一步（整个循环分四步）
  var _step = 0;

  ///当前步骤的进度值
  var _stepProgress = 0.0;

  ///翻转中的那一半卡片在y轴上的投影距离
  var projectedHeight = 0.0;

  ///色卡半径
  var _radius = 0.0;

  /// 这个方法实现绘制，之前在widget中了解过paint方法在renderObject中是带偏移量的而这里没有
  /// 看源码可以发现在RenderCustomPaint中，在回调之前已经对canvas做了偏移了就是不知道有没有
  /// 约束绘制区域
  @override
  void paint(Canvas canvas, Size size) {
    _step = (progress * 4).toInt();
    _stepProgress = progress * 4 - _step;
    _radius = size.shortestSide / 2;
    //每一步动画中，上半卡片绕x轴旋转180度，这里用三角函数计算卡片在y轴的投影高度
    projectedHeight = cos(_stepProgress * pi).abs() * _radius;

    canvas.save();
    //因为我们的绘制过程基本是基于视图中心的，将中心移动到原点便于设置绘制坐标
    canvas.translate(size.width / 2, size.height / 2);
    //1、绘制下个色卡
    canvas.drawCircle(
      Offset.zero,
      _radius,
      _paint..color = colors[_nextColorIndex],
    );
    //四个方向的翻转
    //flutter api是弧度角，二分之pi表示90度，负号表示逆时针旋转画布
    canvas.rotate(_step * pi / -2);
    //2、绘制当前色卡的下半部分
    canvas.drawArc(
        Rect.fromCircle(center: Offset.zero, radius: _radius),
        0, //阿西吧，谁写的api，写的angle实际是弧度角
        pi,
        true,
        _paint..color = colors[_currColorIndex]);
    //3、绘制翻转中的另一半色卡
    if (_stepProgress < 0.5) {
      //不可避免的浮点值问题，会导致上下两半的图形中间留了一条缝隙，这里通过绘制一像素的线条进行修复
      canvas.drawLine(
        Offset(-_radius, 0),
        Offset(_radius, 0),
        _paint..color = colors[_currColorIndex],
      );
      canvas.drawArc(
          Rect.fromCenter(
              center: Offset.zero,
              width: _radius * 2,
              height: projectedHeight * 2),
          pi,
          pi,
          true,
          _paint..color = colors[_currColorIndex]);
    } else {
      canvas.drawLine(
        Offset(-_radius, 0),
        Offset(_radius, 0),
        _paint..color = colors[_nextColorIndex],
      );
      canvas.drawArc(
          Rect.fromCenter(
              center: Offset.zero,
              width: _radius * 2,
              height: projectedHeight * 2),
          0,
          pi,
          true,
          _paint..color = colors[_nextColorIndex]);
    }
    canvas.restore();
  }
}
